const toString = (obj) => Object.prototype.toString.call(obj);

const isPlainObject = (obj) =>
    toString(obj) === '[object Object]';

const isArray = (obj) =>
    toString(obj) === '[object Array]';

const isFunction = (obj) =>
    toString(obj) === '[object Function]';

const isStirngLiteral = (str) => typeof str === 'string';

const keys = (obj) => {
    return Reflect.ownKeys(obj);
}

/** 深拷贝：直接改变源对象 */
const deepMerge = (src, target) => {
    if (!isPlainObject(src) || !isPlainObject(target)) return;
    for (const key in target) {
        if (isPlainObject(target[key])) {
            if (!isPlainObject(src[key])) {
                src[key] = {};
            }
            deepMerge(src[key], target[key]);
        } else {
            src[key] = target[key];
        }
    }
}

/* 创建range */
function createRange(node, offset) {
    const range = document.createRange();
    range.setStart(node, offset);
    range.setEnd(node, offset);
    return range;
}

/**
 * 替换 range
 * @param {Range} range
 */
function replaceContent(range, node) {
    range.insertNode(node);
    range.setStartAfter(node);
    range.deleteContents();
    range.setStartBefore(node);
    range.setEndAfter(node);
}

/* 设置属性 */
function setAttr(root, key, value) {
    const onReg = /^on(\w+)/;
    const matched = key.match(onReg);
    if (matched) {
        root.addEventListener(matched[1].toLowerCase(), value);
    } else if (key === 'className') {
        root.setAttribute('class', value);
    } else {
        root.setAttribute(key, value);
    }
}

/**
 * compare
 * @param {ElementWrapper} oldNode 
 * @param {ElementWrapper} newNode 
 */
function isSameNode(oldNode, newNode) {
    if (oldNode.type !== newNode.type) return false;
    if (keys(oldNode.props).length !== keys(newNode.props).length) return false;

    for (const key of keys(oldNode.props)) {
        if (oldNode.props[key] !== newNode.props[key]) return false;
    }
    return true;
}

const NULL = Object.create(null);
const RENDER_TO_DOM = Symbol('render to dom');

const IS_CLASS_COMPONENT = Symbol('is class component');
// 检查一个类（函数）是否是Component组件的派生类
const isClassComponent = (func) => {
    return isFunction(func) && !!func[IS_CLASS_COMPONENT]
}

export class Component {
    constructor(props) {
        this.props = props || NULL;
        this.children = [];
    }

    get vdom() {
        const root = this.render();
        return root.vdom;
    }

    appendChild(component) {
        this.children.push(component);
    }
    [RENDER_TO_DOM](range) {
        this.oldRange = range;
        this.oldVdom = this.vdom;
        this.oldVdom[RENDER_TO_DOM](range);
    }
    setState(newState) {
        if (isPlainObject(this.state)) {
            deepMerge(this.state, newState);
        } else {
            this.state = newState;
        }
        this.update();
    }
    update() {
        const newVdom = this.vdom;
        update(this.oldVdom, newVdom);
        this.oldVdom = newVdom;

        function update(oldNode, newNode) {
            if (isSameNode(oldNode, newNode)) {
                // diff
                const newChildren = newNode.vchildren;
                const oldChildren = oldNode.vchildren;
                if (newChildren.length && oldChildren.length) {
                    newNode.oldRange = oldNode.oldRange;

                    let tailRange = oldChildren.slice(-1)[0].oldRange;
                    compare(oldChildren, newChildren);
                    function compare(oldChildren, newChildren) {
                        for (let i = 0; i < newChildren.length; i++) {
                            const newChild = newChildren[i];
                            const oldChild = oldChildren[i];
                            if (i < oldChildren.length) {
                                update(oldChild, newChild);
                            } else {
                                const range = document.createRange();
                                range.setStart(tailRange.endContainer, tailRange.endOffset);
                                range.setEnd(tailRange.endContainer, tailRange.endOffset);
                                newChild[RENDER_TO_DOM](range);
                                tailRange = range;
                            }
                        }
                    }
                    return;
                }
            }

            // fallback
            newNode[RENDER_TO_DOM](oldNode.oldRange);
        }

    }
}
Component[IS_CLASS_COMPONENT] = {};

class ElementWrapper extends Component {
    constructor(type, props) {
        super(props);
        this.type = type;
    }

    get vdom() {
        this.vchildren = this.children.map(child => child.vdom);
        return this;
    }

    [RENDER_TO_DOM](range) {
        this.oldRange = range;

        /** @type HTMLElement */
        const root = document.createElement(this.type);

        if (isPlainObject(this.props)) {
            for (const key of keys(this.props)) {
                setAttr(root, key, this.props[key]);
            }
        }

        for (const child of this.vchildren) {
            const childRange = createRange(root, root.childNodes.length);
            child[RENDER_TO_DOM](childRange);
        }

        replaceContent(range, root);
    }
}

class TextWrapper extends Component {
    constructor(content) {
        super();
        this.type = '#text';
        this.content = content;
    }

    get vdom() {
        this.vchildren = this.children.map(child => child.vdom);
        return this;
    }

    [RENDER_TO_DOM](range) {
        const root = document.createTextNode(this.content);
        this.oldRange = range;
        replaceContent(range, root);
    }
}


export function createElement(type, attrs, ...children) {
    /** @type Component */
    let element;
    if (isClassComponent(type)) {
        element = new type({ ...attrs });
    } else if (isFunction(type)) {
        element = type({ ...attrs }); // TODO 函数组件 children
        return element;
    } else {
        element = new ElementWrapper(type, { ...attrs });
    }

    const appendChildren = (children) => {
        children.forEach(child => {
            if (child == null) return;
            if (isArray(child)) {
                appendChildren(child);
            } else if (child instanceof Component) {
                element.appendChild(child);
            } else {
                element.appendChild(new TextWrapper(child));
            }
        })
    }
    appendChildren(children);

    return element;
}

export function render(element, parentElement = document.body) {
    const range = document.createRange();
    range.setStart(parentElement, 0);
    range.setEnd(parentElement, parentElement.childNodes.length);
    range.deleteContents();
    element[RENDER_TO_DOM](range);
}